/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.badlogic.gdx.scenes.scene2d.ui;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData;
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.graphics.g2d.GlyphLayout.GlyphRun;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.scenes.scene2d.utils.Disableable;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.scenes.scene2d.utils.UIUtils;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Clipboard;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.Null;
import com.badlogic.gdx.utils.Pools;
import com.badlogic.gdx.utils.Timer;
import com.badlogic.gdx.utils.Timer.Task;

/** A single-line text input field.
 * <p>
 * The preferred height of a text field is the height of the {@link TextFieldStyle#font} and {@link TextFieldStyle#background}.
 * The preferred width of a text field is 150, a relatively arbitrary size.
 * <p>
 * The text field will copy the currently selected text when ctrl+c is pressed, and paste any text in the clipboard when ctrl+v is
 * pressed. Clipboard functionality is provided via the {@link Clipboard} interface. Currently there are two standard
 * implementations, one for the desktop and one for Android. The Android clipboard is a stub, as copy & pasting on Android is not
 * supported yet.
 * <p>
 * The text field allows you to specify an {@link OnscreenKeyboard} for displaying a softkeyboard and piping all key events
 * generated by the keyboard to the text field. There are two standard implementations, one for the desktop and one for Android.
 * The desktop keyboard is a stub, as a softkeyboard is not needed on the desktop. The Android {@link OnscreenKeyboard}
 * implementation will bring up the default IME.
 * @author mzechner
 * @author Nathan Sweet */
public class TextField extends Widget implements Disableable {
    static protected final char BACKSPACE = 8;
    static protected final char CARRIAGE_RETURN = '\r';
    static protected final char NEWLINE = '\n';
    static protected final char TAB = '\t';
    static protected final char DELETE = 127;
    static protected final char BULLET = 149;

    static private final Vector2 tmp1 = new Vector2();
    static private final Vector2 tmp2 = new Vector2();
    static private final Vector2 tmp3 = new Vector2();

    static public float keyRepeatInitialTime = 0.4f;
    static public float keyRepeatTime = 0.1f;

    protected String text;
    protected int cursor, selectionStart;
    protected boolean hasSelection;
    protected boolean writeEnters;
    protected final GlyphLayout layout = new GlyphLayout();
    protected final FloatArray glyphPositions = new FloatArray();

    TextFieldStyle style;
    private String messageText;
    protected CharSequence displayText;
    Clipboard clipboard;
    InputListener inputListener;
    @Null TextFieldListener listener;
    @Null TextFieldFilter filter;
    OnscreenKeyboard keyboard = new DefaultOnscreenKeyboard();
    boolean focusTraversal = true, onlyFontChars = true, disabled;
    private int textHAlign = Align.left;
    private float selectionX, selectionWidth;

    String undoText = "";
    long lastChangeTime;

    boolean passwordMode;
    private StringBuilder passwordBuffer;
    private char passwordCharacter = BULLET;

    protected float fontOffset, textHeight, textOffset;
    float renderOffset;
    protected int visibleTextStart, visibleTextEnd;
    private int maxLength;

    boolean focused;
    boolean cursorOn;
    float blinkTime = 0.32f;
    final Task blinkTask = new Task() {
        public void run () {
            if (getStage() == null) {
                cancel();
                return;
            }
            cursorOn = !cursorOn;
            Gdx.graphics.requestRendering();
        }
    };

    final KeyRepeatTask keyRepeatTask = new KeyRepeatTask();
    boolean programmaticChangeEvents;

    public TextField (@Null String text, Skin skin) {
        this(text, skin.get(TextFieldStyle.class));
    }

    public TextField (@Null String text, Skin skin, String styleName) {
        this(text, skin.get(styleName, TextFieldStyle.class));
    }

    public TextField (@Null String text, TextFieldStyle style) {
        setStyle(style);
        clipboard = Gdx.app.getClipboard();
        initialize();
        setText(text);
        setSize(getPrefWidth(), getPrefHeight());
    }

    protected void initialize () {
        addListener(inputListener = createInputListener());
    }

    protected InputListener createInputListener () {
        return new TextFieldClickListener();
    }

    protected int letterUnderCursor (float x) {
        x -= textOffset + fontOffset - style.font.getData().cursorX - glyphPositions.get(visibleTextStart);
        Drawable background = getBackgroundDrawable();
        if (background != null) x -= style.background.getLeftWidth();
        int n = this.glyphPositions.size;
        float[] glyphPositions = this.glyphPositions.items;
        for (int i = 1; i < n; i++) {
            if (glyphPositions[i] > x) {
                if (glyphPositions[i] - x <= x - glyphPositions[i - 1]) return i;
                return i - 1;
            }
        }
        return n - 1;
    }

    protected boolean isWordCharacter (char c) {
        return Character.isLetterOrDigit(c);
    }

    protected int[] wordUnderCursor (int at) {
        String text = this.text;
        int start = at, right = text.length(), left = 0, index = start;
        if (at >= text.length()) {
            left = text.length();
            right = 0;
        } else {
            for (; index < right; index++) {
                if (!isWordCharacter(text.charAt(index))) {
                    right = index;
                    break;
                }
            }
            for (index = start - 1; index > -1; index--) {
                if (!isWordCharacter(text.charAt(index))) {
                    left = index + 1;
                    break;
                }
            }
        }
        return new int[] {left, right};
    }

    int[] wordUnderCursor (float x) {
        return wordUnderCursor(letterUnderCursor(x));
    }

    boolean withinMaxLength (int size) {
        return maxLength <= 0 || size < maxLength;
    }

    public void setMaxLength (int maxLength) {
        this.maxLength = maxLength;
    }

    public int getMaxLength () {
        return this.maxLength;
    }

    /** When false, text set by {@link #setText(String)} may contain characters not in the font, a space will be displayed instead.
     * When true (the default), characters not in the font are stripped by setText. Characters not in the font are always stripped
     * when typed or pasted. */
    public void setOnlyFontChars (boolean onlyFontChars) {
        this.onlyFontChars = onlyFontChars;
    }

    public void setStyle (TextFieldStyle style) {
        if (style == null) throw new IllegalArgumentException("style cannot be null.");
        this.style = style;

        textHeight = style.font.getCapHeight() - style.font.getDescent() * 2;
        if (text != null) updateDisplayText();
        invalidateHierarchy();
    }

    /** Returns the text field's style. Modifying the returned style may not have an effect until {@link #setStyle(TextFieldStyle)}
     * is called. */
    public TextFieldStyle getStyle () {
        return style;
    }

    protected void calculateOffsets () {
        float visibleWidth = getWidth();
        Drawable background = getBackgroundDrawable();
        if (background != null) visibleWidth -= background.getLeftWidth() + background.getRightWidth();

        int glyphCount = glyphPositions.size;
        float[] glyphPositions = this.glyphPositions.items;

        // Check if the cursor has gone out the left or right side of the visible area and adjust renderOffset.
        cursor = MathUtils.clamp(cursor, 0, glyphCount - 1);
        float distance = glyphPositions[Math.max(0, cursor - 1)] + renderOffset;
        if (distance <= 0)
            renderOffset -= distance;
        else {
            int index = Math.min(glyphCount - 1, cursor + 1);
            float minX = glyphPositions[index] - visibleWidth;
            if (-renderOffset < minX) renderOffset = -minX;
        }

        // Prevent renderOffset from starting too close to the end, eg after text was deleted.
        float maxOffset = 0;
        float width = glyphPositions[glyphCount - 1];
        for (int i = glyphCount - 2; i >= 0; i--) {
            float x = glyphPositions[i];
            if (width - x > visibleWidth) break;
            maxOffset = x;
        }
        if (-renderOffset > maxOffset) renderOffset = -maxOffset;

        // calculate first visible char based on render offset
        visibleTextStart = 0;
        float startX = 0;
        for (int i = 0; i < glyphCount; i++) {
            if (glyphPositions[i] >= -renderOffset) {
                visibleTextStart = i;
                startX = glyphPositions[i];
                break;
            }
        }

        // calculate last visible char based on visible width and render offset
        int end = visibleTextStart + 1;
        float endX = visibleWidth - renderOffset;
        for (int n = Math.min(displayText.length(), glyphCount); end <= n; end++)
            if (glyphPositions[end] > endX) break;
        visibleTextEnd = Math.max(0, end - 1);

        if ((textHAlign & Align.left) == 0) {
            textOffset = visibleWidth - glyphPositions[visibleTextEnd] - fontOffset + startX;
            if ((textHAlign & Align.center) != 0) textOffset = Math.round(textOffset * 0.5f);
        } else
            textOffset = startX + renderOffset;

        // calculate selection x position and width
        if (hasSelection) {
            int minIndex = Math.min(cursor, selectionStart);
            int maxIndex = Math.max(cursor, selectionStart);
            float minX = Math.max(glyphPositions[minIndex] - glyphPositions[visibleTextStart], -textOffset);
            float maxX = Math.min(glyphPositions[maxIndex] - glyphPositions[visibleTextStart], visibleWidth - textOffset);
            selectionX = minX;
            selectionWidth = maxX - minX - style.font.getData().cursorX;
        }
    }

    protected @Null Drawable getBackgroundDrawable () {
        if (disabled && style.disabledBackground != null) return style.disabledBackground;
        if (style.focusedBackground != null && hasKeyboardFocus()) return style.focusedBackground;
        return style.background;
    }

    public void draw (Batch batch, float parentAlpha) {
        boolean focused = hasKeyboardFocus();
        if (focused != this.focused || (focused && !blinkTask.isScheduled())) {
            this.focused = focused;
            blinkTask.cancel();
            cursorOn = focused;
            if (focused)
                Timer.schedule(blinkTask, blinkTime, blinkTime);
            else
                keyRepeatTask.cancel();
        } else if (!focused) //
            cursorOn = false;

        final BitmapFont font = style.font;
        final Color fontColor = (disabled && style.disabledFontColor != null) ? style.disabledFontColor
            : ((focused && style.focusedFontColor != null) ? style.focusedFontColor : style.fontColor);
        final Drawable selection = style.selection;
        final Drawable cursorPatch = style.cursor;
        final Drawable background = getBackgroundDrawable();

        Color color = getColor();
        float x = getX();
        float y = getY();
        float width = getWidth();
        float height = getHeight();

        batch.setColor(color.r, color.g, color.b, color.a * parentAlpha);
        float bgLeftWidth = 0, bgRightWidth = 0;
        if (background != null) {
            background.draw(batch, x, y, width, height);
            bgLeftWidth = background.getLeftWidth();
            bgRightWidth = background.getRightWidth();
        }

        float textY = getTextY(font, background);
        calculateOffsets();

        if (focused && hasSelection && selection != null) {
            drawSelection(selection, batch, font, x + bgLeftWidth, y + textY);
        }

        float yOffset = font.isFlipped() ? -textHeight : 0;
        if (displayText.length() == 0) {
            if ((!focused || disabled) && messageText != null) {
                BitmapFont messageFont = style.messageFont != null ? style.messageFont : font;
                if (style.messageFontColor != null) {
                    messageFont.setColor(style.messageFontColor.r, style.messageFontColor.g, style.messageFontColor.b,
                        style.messageFontColor.a * color.a * parentAlpha);
                } else
                    messageFont.setColor(0.7f, 0.7f, 0.7f, color.a * parentAlpha);
                drawMessageText(batch, messageFont, x + bgLeftWidth, y + textY + yOffset, width - bgLeftWidth - bgRightWidth);
            }
        } else {
            font.setColor(fontColor.r, fontColor.g, fontColor.b, fontColor.a * color.a * parentAlpha);
            drawText(batch, font, x + bgLeftWidth, y + textY + yOffset);
        }
        if (!disabled && cursorOn && cursorPatch != null) {
            drawCursor(cursorPatch, batch, font, x + bgLeftWidth, y + textY);
        }
    }

    protected float getTextY (BitmapFont font, @Null Drawable background) {
        float height = getHeight();
        float textY = textHeight / 2 + font.getDescent();
        if (background != null) {
            float bottom = background.getBottomHeight();
            textY = textY + (height - background.getTopHeight() - bottom) / 2 + bottom;
        } else {
            textY = textY + height / 2;
        }
        if (font.usesIntegerPositions()) textY = (int)textY;
        return textY;
    }

    /** Draws selection rectangle **/
    protected void drawSelection (Drawable selection, Batch batch, BitmapFont font, float x, float y) {
        selection.draw(batch, x + textOffset + selectionX + fontOffset, y - textHeight - font.getDescent(), selectionWidth,
            textHeight);
    }

    protected void drawText (Batch batch, BitmapFont font, float x, float y) {
        font.draw(batch, displayText, x + textOffset, y, visibleTextStart, visibleTextEnd, 0, Align.left, false);
    }

    protected void drawMessageText (Batch batch, BitmapFont font, float x, float y, float maxWidth) {
        font.draw(batch, messageText, x, y, 0, messageText.length(), maxWidth, textHAlign, false, "...");
    }

    protected void drawCursor (Drawable cursorPatch, Batch batch, BitmapFont font, float x, float y) {
        cursorPatch.draw(batch,
            x + textOffset + glyphPositions.get(cursor) - glyphPositions.get(visibleTextStart) + fontOffset + font.getData().cursorX,
            y - textHeight - font.getDescent(), cursorPatch.getMinWidth(), textHeight);
    }

    void updateDisplayText () {
        BitmapFont font = style.font;
        BitmapFontData data = font.getData();
        String text = this.text;
        int textLength = text.length();

        StringBuilder buffer = new StringBuilder();
        for (int i = 0; i < textLength; i++) {
            char c = text.charAt(i);
            buffer.append(data.hasGlyph(c) ? c : ' ');
        }
        String newDisplayText = buffer.toString();

        if (passwordMode && data.hasGlyph(passwordCharacter)) {
            if (passwordBuffer == null) passwordBuffer = new StringBuilder(newDisplayText.length());
            if (passwordBuffer.length() > textLength)
                passwordBuffer.setLength(textLength);
            else {
                for (int i = passwordBuffer.length(); i < textLength; i++)
                    passwordBuffer.append(passwordCharacter);
            }
            displayText = passwordBuffer;
        } else
            displayText = newDisplayText;

        layout.setText(font, displayText.toString().replace('\r', ' ').replace('\n', ' '));
        glyphPositions.clear();
        float x = 0;
        if (layout.runs.size > 0) {
            GlyphRun run = layout.runs.first();
            FloatArray xAdvances = run.xAdvances;
            fontOffset = xAdvances.first();
            for (int i = 1, n = xAdvances.size; i < n; i++) {
                glyphPositions.add(x);
                x += xAdvances.get(i);
            }
        } else
            fontOffset = 0;
        glyphPositions.add(x);

        visibleTextStart = Math.min(visibleTextStart, glyphPositions.size - 1);
        visibleTextEnd = MathUtils.clamp(visibleTextEnd, visibleTextStart, glyphPositions.size - 1);

        if (selectionStart > newDisplayText.length()) selectionStart = textLength;
    }

    /** Copies the contents of this TextField to the {@link Clipboard} implementation set on this TextField. */
    public void copy () {
        if (hasSelection && !passwordMode) {
            clipboard.setContents(text.substring(Math.min(cursor, selectionStart), Math.max(cursor, selectionStart)));
        }
    }

    /** Copies the selected contents of this TextField to the {@link Clipboard} implementation set on this TextField, then removes
     * it. */
    public void cut () {
        cut(programmaticChangeEvents);
    }

    void cut (boolean fireChangeEvent) {
        if (hasSelection && !passwordMode) {
            copy();
            cursor = delete(fireChangeEvent);
            updateDisplayText();
        }
    }

    void paste (@Null String content, boolean fireChangeEvent) {
        if (content == null) return;
        StringBuilder buffer = new StringBuilder();
        int textLength = text.length();
        if (hasSelection) textLength -= Math.abs(cursor - selectionStart);
        BitmapFontData data = style.font.getData();
        for (int i = 0, n = content.length(); i < n; i++) {
            if (!withinMaxLength(textLength + buffer.length())) break;
            char c = content.charAt(i);
            if (!(writeEnters && (c == NEWLINE || c == CARRIAGE_RETURN))) {
                if (c == '\r' || c == '\n') continue;
                if (onlyFontChars && !data.hasGlyph(c)) continue;
                if (filter != null && !filter.acceptChar(this, c)) continue;
            }
            buffer.append(c);
        }
        content = buffer.toString();

        if (hasSelection) cursor = delete(fireChangeEvent);
        if (fireChangeEvent)
            changeText(text, insert(cursor, content, text));
        else
            text = insert(cursor, content, text);
        updateDisplayText();
        cursor += content.length();
    }

    String insert (int position, CharSequence text, String to) {
        if (to.length() == 0) return text.toString();
        return to.substring(0, position) + text + to.substring(position, to.length());
    }

    int delete (boolean fireChangeEvent) {
        int from = selectionStart;
        int to = cursor;
        int minIndex = Math.min(from, to);
        int maxIndex = Math.max(from, to);
        String newText = (minIndex > 0 ? text.substring(0, minIndex) : "")
            + (maxIndex < text.length() ? text.substring(maxIndex, text.length()) : "");
        if (fireChangeEvent)
            changeText(text, newText);
        else
            text = newText;
        clearSelection();
        return minIndex;
    }

    /** Sets the {@link Stage#setKeyboardFocus(Actor) keyboard focus} to the next TextField. If no next text field is found, the
     * onscreen keyboard is hidden. Does nothing if the text field is not in a stage.
     * @param up If true, the text field with the same or next smallest y coordinate is found, else the next highest. */
    public void next (boolean up) {
        Stage stage = getStage();
        if (stage == null) return;
        TextField current = this;
        Vector2 currentCoords = current.getParent().localToStageCoordinates(tmp2.set(current.getX(), current.getY()));
        Vector2 bestCoords = tmp1;
        while (true) {
            TextField textField = current.findNextTextField(stage.getActors(), null, bestCoords, currentCoords, up);
            if (textField == null) { // Try to wrap around.
                if (up)
                    currentCoords.set(-Float.MAX_VALUE, -Float.MAX_VALUE);
                else
                    currentCoords.set(Float.MAX_VALUE, Float.MAX_VALUE);
                textField = current.findNextTextField(stage.getActors(), null, bestCoords, currentCoords, up);
            }
            if (textField == null) {
                Gdx.input.setOnscreenKeyboardVisible(false);
                break;
            }
            if (stage.setKeyboardFocus(textField)) {
                textField.selectAll();
                break;
            }
            current = textField;
            currentCoords.set(bestCoords);
        }
    }

    /** @return May be null. */
    private @Null TextField findNextTextField (Array<Actor> actors, @Null TextField best, Vector2 bestCoords,
                                               Vector2 currentCoords, boolean up) {
        for (int i = 0, n = actors.size; i < n; i++) {
            Actor actor = actors.get(i);
            if (actor instanceof TextField) {
                if (actor == this) continue;
                TextField textField = (TextField)actor;
                if (textField.isDisabled() || !textField.focusTraversal || !textField.ascendantsVisible()) continue;
                Vector2 actorCoords = actor.getParent().localToStageCoordinates(tmp3.set(actor.getX(), actor.getY()));
                boolean below = actorCoords.y != currentCoords.y && (actorCoords.y < currentCoords.y ^ up);
                boolean right = actorCoords.y == currentCoords.y && (actorCoords.x > currentCoords.x ^ up);
                if (!below && !right) continue;
                boolean better = best == null || (actorCoords.y != bestCoords.y && (actorCoords.y > bestCoords.y ^ up));
                if (!better) better = actorCoords.y == bestCoords.y && (actorCoords.x < bestCoords.x ^ up);
                if (better) {
                    best = (TextField)actor;
                    bestCoords.set(actorCoords);
                }
            } else if (actor instanceof Group)
                best = findNextTextField(((Group)actor).getChildren(), best, bestCoords, currentCoords, up);
        }
        return best;
    }

    public InputListener getDefaultInputListener () {
        return inputListener;
    }

    /** @param listener May be null. */
    public void setTextFieldListener (@Null TextFieldListener listener) {
        this.listener = listener;
    }

    /** @param filter May be null. */
    public void setTextFieldFilter (@Null TextFieldFilter filter) {
        this.filter = filter;
    }

    public @Null TextFieldFilter getTextFieldFilter () {
        return filter;
    }

    /** If true (the default), tab/shift+tab will move to the next text field. */
    public void setFocusTraversal (boolean focusTraversal) {
        this.focusTraversal = focusTraversal;
    }

    public boolean getFocusTraversal () {
        return focusTraversal;
    }

    /** @return May be null. */
    public @Null String getMessageText () {
        return messageText;
    }

    /** Sets the text that will be drawn in the text field if no text has been entered.
     * @param messageText may be null. */
    public void setMessageText (@Null String messageText) {
        this.messageText = messageText;
    }

    /** @param str If null, "" is used. */
    public void appendText (@Null String str) {
        if (str == null) str = "";

        clearSelection();
        cursor = text.length();
        paste(str, programmaticChangeEvents);
    }

    /** @param str If null, "" is used. */
    public void setText (@Null String str) {
        if (str == null) str = "";
        if (str.equals(text)) return;

        clearSelection();
        String oldText = text;
        text = "";
        paste(str, false);
        if (programmaticChangeEvents) changeText(oldText, text);
        cursor = 0;
    }

    /** @return Never null, might be an empty string. */
    public String getText () {
        return text;
    }

    /** @return True if the text was changed. */
    boolean changeText (String oldText, String newText) {
        if (newText.equals(oldText)) return false;
        text = newText;
        ChangeEvent changeEvent = Pools.obtain(ChangeEvent.class);
        boolean cancelled = fire(changeEvent);
        if (cancelled) text = oldText;
        Pools.free(changeEvent);
        return !cancelled;
    }

    /** If false, methods that change the text will not fire {@link ChangeEvent}, the event will be fired only when the user
     * changes the text. */
    public void setProgrammaticChangeEvents (boolean programmaticChangeEvents) {
        this.programmaticChangeEvents = programmaticChangeEvents;
    }

    public boolean getProgrammaticChangeEvents () {
        return programmaticChangeEvents;
    }

    public int getSelectionStart () {
        return selectionStart;
    }

    public String getSelection () {
        return hasSelection ? text.substring(Math.min(selectionStart, cursor), Math.max(selectionStart, cursor)) : "";
    }

    /** Sets the selected text. */
    public void setSelection (int selectionStart, int selectionEnd) {
        if (selectionStart < 0) throw new IllegalArgumentException("selectionStart must be >= 0");
        if (selectionEnd < 0) throw new IllegalArgumentException("selectionEnd must be >= 0");
        selectionStart = Math.min(text.length(), selectionStart);
        selectionEnd = Math.min(text.length(), selectionEnd);
        if (selectionEnd == selectionStart) {
            clearSelection();
            return;
        }
        if (selectionEnd < selectionStart) {
            int temp = selectionEnd;
            selectionEnd = selectionStart;
            selectionStart = temp;
        }

        hasSelection = true;
        this.selectionStart = selectionStart;
        cursor = selectionEnd;
    }

    public void selectAll () {
        setSelection(0, text.length());
    }

    public void clearSelection () {
        hasSelection = false;
    }

    /** Sets the cursor position and clears any selection. */
    public void setCursorPosition (int cursorPosition) {
        if (cursorPosition < 0) throw new IllegalArgumentException("cursorPosition must be >= 0");
        clearSelection();
        cursor = Math.min(cursorPosition, text.length());
    }

    public int getCursorPosition () {
        return cursor;
    }

    /** Default is an instance of {@link DefaultOnscreenKeyboard}. */
    public OnscreenKeyboard getOnscreenKeyboard () {
        return keyboard;
    }

    public void setOnscreenKeyboard (OnscreenKeyboard keyboard) {
        this.keyboard = keyboard;
    }

    public void setClipboard (Clipboard clipboard) {
        this.clipboard = clipboard;
    }

    public float getPrefWidth () {
        return 150;
    }

    public float getPrefHeight () {
        float topAndBottom = 0, minHeight = 0;
        if (style.background != null) {
            topAndBottom = Math.max(topAndBottom, style.background.getBottomHeight() + style.background.getTopHeight());
            minHeight = Math.max(minHeight, style.background.getMinHeight());
        }
        if (style.focusedBackground != null) {
            topAndBottom = Math.max(topAndBottom,
                style.focusedBackground.getBottomHeight() + style.focusedBackground.getTopHeight());
            minHeight = Math.max(minHeight, style.focusedBackground.getMinHeight());
        }
        if (style.disabledBackground != null) {
            topAndBottom = Math.max(topAndBottom,
                style.disabledBackground.getBottomHeight() + style.disabledBackground.getTopHeight());
            minHeight = Math.max(minHeight, style.disabledBackground.getMinHeight());
        }
        return Math.max(topAndBottom + textHeight, minHeight);
    }

    /** Sets text horizontal alignment (left, center or right).
     * @see Align */
    public void setAlignment (int alignment) {
        this.textHAlign = alignment;
    }

    public int getAlignment () {
        return textHAlign;
    }

    /** If true, the text in this text field will be shown as bullet characters.
     * @see #setPasswordCharacter(char) */
    public void setPasswordMode (boolean passwordMode) {
        this.passwordMode = passwordMode;
        updateDisplayText();
    }

    public boolean isPasswordMode () {
        return passwordMode;
    }

    /** Sets the password character for the text field. The character must be present in the {@link BitmapFont}. Default is 149
     * (bullet). */
    public void setPasswordCharacter (char passwordCharacter) {
        this.passwordCharacter = passwordCharacter;
        if (passwordMode) updateDisplayText();
    }

    public void setBlinkTime (float blinkTime) {
        this.blinkTime = blinkTime;
    }

    public void setDisabled (boolean disabled) {
        this.disabled = disabled;
    }

    public boolean isDisabled () {
        return disabled;
    }

    protected void moveCursor (boolean forward, boolean jump) {
        int limit = forward ? text.length() : 0;
        int charOffset = forward ? 0 : -1;
        while ((forward ? ++cursor < limit : --cursor > limit) && jump) {
            if (!continueCursor(cursor, charOffset)) break;
        }
    }

    protected boolean continueCursor (int index, int offset) {
        char c = text.charAt(index + offset);
        return isWordCharacter(c);
    }

    class KeyRepeatTask extends Task {
        int keycode;

        public void run () {
            if (getStage() == null) {
                cancel();
                return;
            }
            inputListener.keyDown(null, keycode);
        }
    }

    /** Interface for listening to typed characters.
     * @author mzechner */
    static public interface TextFieldListener {
        public void keyTyped (TextField textField, char c);
    }

    /** Interface for filtering characters entered into the text field.
     * @author mzechner */
    static public interface TextFieldFilter {
        public boolean acceptChar (TextField textField, char c);

        static public class DigitsOnlyFilter implements TextFieldFilter {
            public boolean acceptChar (TextField textField, char c) {
                return Character.isDigit(c);
            }
        }
    }

    /** An interface for onscreen keyboards. Can invoke the default keyboard or render your own keyboard!
     * @author mzechner */
    static public interface OnscreenKeyboard {
        public void show (boolean visible);
    }

    /** The default {@link OnscreenKeyboard} used by all {@link TextField} instances. Just uses
     * {@link Input#setOnscreenKeyboardVisible(boolean)} as appropriate. Might overlap your actual rendering, so use with care!
     * @author mzechner */
    static public class DefaultOnscreenKeyboard implements OnscreenKeyboard {
        public void show (boolean visible) {
            Gdx.input.setOnscreenKeyboardVisible(visible);
        }
    }

    /** Basic input listener for the text field */
    public class TextFieldClickListener extends ClickListener {
        public void clicked (InputEvent event, float x, float y) {
            int count = getTapCount() % 4;
            if (count == 0) clearSelection();
            if (count == 2) {
                int[] array = wordUnderCursor(x);
                setSelection(array[0], array[1]);
            }
            if (count == 3) selectAll();
            Stage stage = getStage();
            if (!disabled && stage != null) stage.setKeyboardFocus(TextField.this);
        }

        public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
            if (!super.touchDown(event, x, y, pointer, button)) return false;
            if (pointer == 0 && button != 0) return false;
            if (disabled) return true;
            setCursorPosition(x, y);
            selectionStart = cursor;
            
            keyboard.show(true);
            hasSelection = true;
            return true;
        }

        public void touchDragged (InputEvent event, float x, float y, int pointer) {
            super.touchDragged(event, x, y, pointer);
            setCursorPosition(x, y);
        }

        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
            if (selectionStart == cursor) hasSelection = false;
            super.touchUp(event, x, y, pointer, button);
        }

        protected void setCursorPosition (float x, float y) {
            cursor = letterUnderCursor(x);

            cursorOn = focused;
            blinkTask.cancel();
            if (focused) Timer.schedule(blinkTask, blinkTime, blinkTime);
        }

        protected void goHome (boolean jump) {
            cursor = 0;
        }

        protected void goEnd (boolean jump) {
            cursor = text.length();
        }

        public boolean keyDown (InputEvent event, int keycode) {
            if (disabled) return false;

            cursorOn = focused;
            blinkTask.cancel();
            if (focused) Timer.schedule(blinkTask, blinkTime, blinkTime);

            if (!hasKeyboardFocus()) return false;

            boolean repeat = false;
            boolean ctrl = UIUtils.ctrl();
            boolean jump = ctrl && !passwordMode;
            boolean handled = true;

            if (ctrl) {
                switch (keycode) {
                    case Keys.V:
                        paste(clipboard.getContents(), true);
                        repeat = true;
                        break;
                    case Keys.C:
                    case Keys.INSERT:
                        copy();
                        return true;
                    case Keys.X:
                        cut(true);
                        return true;
                    case Keys.A:
                        selectAll();
                        return true;
                    default:
                        handled = false;
                }
            }

            if (UIUtils.shift()) {
                switch (keycode) {
                    case Keys.INSERT:
                        paste(clipboard.getContents(), true);
                        break;
                    case Keys.FORWARD_DEL:
                        cut(true);
                        break;
                }

                selection:
                {
                    int temp = cursor;
                    keys:
                    {
                        switch (keycode) {
                            case Keys.LEFT:
                                moveCursor(false, jump);
                                repeat = true;
                                handled = true;
                                break keys;
                            case Keys.RIGHT:
                                moveCursor(true, jump);
                                repeat = true;
                                handled = true;
                                break keys;
                            case Keys.HOME:
                                goHome(jump);
                                handled = true;
                                break keys;
                            case Keys.END:
                                goEnd(jump);
                                handled = true;
                                break keys;
                        }
                        break selection;
                    }
                    if (!hasSelection) {
                        selectionStart = temp;
                        hasSelection = true;
                    }
                }
            } else {
                // Cursor movement or other keys (kills selection).
                switch (keycode) {
                    case Keys.LEFT:
                        moveCursor(false, jump);
                        clearSelection();
                        repeat = true;
                        handled = true;
                        break;
                    case Keys.RIGHT:
                        moveCursor(true, jump);
                        clearSelection();
                        repeat = true;
                        handled = true;
                        break;
                    case Keys.HOME:
                        goHome(jump);
                        clearSelection();
                        handled = true;
                        break;
                    case Keys.END:
                        goEnd(jump);
                        clearSelection();
                        handled = true;
                        break;
                }
            }

            cursor = MathUtils.clamp(cursor, 0, text.length());

            if (repeat) scheduleKeyRepeatTask(keycode);
            return handled;
        }

        protected void scheduleKeyRepeatTask (int keycode) {
            if (!keyRepeatTask.isScheduled() || keyRepeatTask.keycode != keycode) {
                keyRepeatTask.keycode = keycode;
                keyRepeatTask.cancel();
                Timer.schedule(keyRepeatTask, keyRepeatInitialTime, keyRepeatTime);
            }
        }

        public boolean keyUp (InputEvent event, int keycode) {
            if (disabled) return false;
            keyRepeatTask.cancel();
            return true;
        }

        /** Checks if focus traversal should be triggered. The default implementation uses {@link TextField#focusTraversal} and the
         * typed character, depending on the OS.
         * @param character The character that triggered a possible focus traversal.
         * @return true if the focus should change to the {@link TextField#next(boolean) next} input field. */
        protected boolean checkFocusTraversal (char character) {
            return focusTraversal && (character == TAB
                || ((character == CARRIAGE_RETURN || character == NEWLINE) && (UIUtils.isAndroid || UIUtils.isIos)));
        }

        public boolean keyTyped (InputEvent event, char character) {
            if (disabled) return false;

            // Disallow "typing" most ASCII control characters, which would show up as a space when onlyFontChars is true.
            switch (character) {
                case BACKSPACE:
                case TAB:
                case NEWLINE:
                case CARRIAGE_RETURN:
                    break;
                default:
                    if (character < 32) return false;
            }

            if (!hasKeyboardFocus()) return false;

            if (UIUtils.isMac && Gdx.input.isKeyPressed(Keys.SYM)) return true;

            if (checkFocusTraversal(character))
                next(UIUtils.shift());
            else {
                boolean enter = character == CARRIAGE_RETURN || character == NEWLINE;
                boolean delete = character == DELETE;
                boolean backspace = character == BACKSPACE;
                boolean add = enter ? writeEnters : (!onlyFontChars || style.font.getData().hasGlyph(character));
                boolean remove = backspace || delete;
                if (add || remove) {
                    String oldText = text;
                    int oldCursor = cursor;
                    if (remove) {
                        if (hasSelection)
                            cursor = delete(false);
                        else {
                            if (backspace && cursor > 0) {
                                text = text.substring(0, cursor - 1) + text.substring(cursor--);
                                renderOffset = 0;
                            }
                            if (delete && cursor < text.length()) {
                                text = text.substring(0, cursor) + text.substring(cursor + 1);
                            }
                        }
                    }
                    if (add && !remove) {
                        // Character may be added to the text.
                        if (!enter && filter != null && !filter.acceptChar(TextField.this, character)) return true;
                        if (!withinMaxLength(text.length() - (hasSelection ? Math.abs(cursor - selectionStart) : 0))) return true;
                        if (hasSelection) cursor = delete(false);
                        String insertion = enter ? "\n" : String.valueOf(character);
                        text = insert(cursor++, insertion, text);
                    }
                    String tempUndoText = undoText;
                    if (changeText(oldText, text)) {
                        long time = System.currentTimeMillis();
                        if (time - 750 > lastChangeTime) undoText = oldText;
                        lastChangeTime = time;
                        updateDisplayText();
                    } else if (!text.equals(oldText)) // Keep cursor movement if the text is the same.
                        cursor = oldCursor;
                }
            }
            if (listener != null) listener.keyTyped(TextField.this, character);
            return true;
        }
    }

    /** The style for a text field, see {@link TextField}.
     * @author mzechner
     * @author Nathan Sweet */
    static public class TextFieldStyle {
        public BitmapFont font;
        public Color fontColor;
        public @Null Color focusedFontColor, disabledFontColor;
        public @Null Drawable background, focusedBackground, disabledBackground, cursor, selection;
        public @Null BitmapFont messageFont;
        public @Null Color messageFontColor;

        public TextFieldStyle () {
        }

        public TextFieldStyle (BitmapFont font, Color fontColor, @Null Drawable cursor, @Null Drawable selection,
                               @Null Drawable background) {
            this.font = font;
            this.fontColor = fontColor;
            this.cursor = cursor;
            this.selection = selection;
            this.background = background;
        }

        public TextFieldStyle (TextFieldStyle style) {
            font = style.font;
            if (style.fontColor != null) fontColor = new Color(style.fontColor);
            if (style.focusedFontColor != null) focusedFontColor = new Color(style.focusedFontColor);
            if (style.disabledFontColor != null) disabledFontColor = new Color(style.disabledFontColor);

            background = style.background;
            focusedBackground = style.focusedBackground;
            disabledBackground = style.disabledBackground;
            cursor = style.cursor;
            selection = style.selection;

            messageFont = style.messageFont;
            if (style.messageFontColor != null) messageFontColor = new Color(style.messageFontColor);
        }
    }
}
